<?php

/**
 * @package Nina
 * @author Andrew Rose <rose.andrew@gmail.com>, John Main <john@thedaddy.org>
 */
class Nina_Registry
{
	/**
	 * The collection of values that this registry contains.
	 * @var array
	 */
	public $registry = array();

	/**
	 * A singleton instance of the registry.
	 * @var Nina_Registry
	 */
	protected static $instance = null;

	/**
	 * Constructor
	 * @param boolean [$use_session=false] Use the session
	 */
	public function __construct($use_session = false) {
		if($use_session) {
			if(isset($_SESSION)) {
				if(!isset($_SESSION['NINA_REGISTRY'])) {
					$_SESSION['NINA_REGISTRY'] = array();
				}
				$this->registry =& $_SESSION['NINA_REGISTRY'];
			}
			else {
				throw new Nina_Exception_Registry('Unable to bind registry to session.  Maybe the session has not been started?');
			}
		}
	}

	/**
	 * Get this registry as a singleton.
	 * @return Nina_Registry
	 */
	public static function getInstance() {
		if (self::$instance === null) {
			self::$instance = new self();
		}
		return self::$instance;
	}
	
	/**
	 * Static get helper
	 * @param mixed $key The key
	 * @return mixed
	 */
	public static function doGet($key) {
		$instance = self::getInstance();
		return $instance->get($key);
	}
	
	/**
	 * Static set helper
	 * @param mixed $key The key
	 * @param mixed $value The value
	 * @return boolean
	 */
	public static function doSet($key, $value) {
		$instance = self::getInstance();
		return $instance->set($key, $value);
	}
	
	/**
	 * Import an array of data into the registry,
	 * @param array $data Input data
	 * @param string [$path=false] Input path to import into.
	 */
	public function import($data, $path=false) {
		if($path) {
			$prefix = $path.'.';
		}
		else {
			$prefix = '';
		}

		if(is_array($data)) {
			foreach ($data as $key => $value) {
				$this->set($prefix.$key, $value);
			}
		}
	}

	/**
	 * Resolve a path to the node there.
	 * @param string $path Path (dot separated)
	 * @return mixed
	 */
	protected function &resolve($path) {
		$null = null;
		$ptr = &$this->registry;
		if (empty($path)) return $ptr;
		$tmp = explode(".", $path);
		foreach($tmp as $node) {
			if (isset($ptr[$node])) {
				$ptr = &$ptr[$node];
			}
			else
			{
				return $null;
			}
		}
		$ptr =& $ptr;
		return $ptr;
	}

	/**
	 * Set a new item in the registry.
	 * Objects will be stored by reference.
	 * @param string $path Path (dot separated)
	 * @param mixed $data Data to store
	 */
	public function set($path, $data) {
		$pointer = FALSE;
		$ptr = &$this->registry;

		if(substr($path, 0, 1)=='*') {
			$path = substr($path, 1, strlen($path));
			$pointer = TRUE;
		}

		$tmp = explode(".", $path);
		foreach($tmp as $node) {
			if (!isset($ptr[$node]) || !is_array($ptr[$node])) {
				$ptr[$node] = array();
			}
			$ptr = &$ptr[$node];
		}

		if($pointer) {
			/* ew ew ew - The problem is the reference returned by resolve can not be used to set the variable it is referencing to 
			reference the path supplied.  So we need to create the array syntax with getArray() and eval it in. */
			eval('$this->registry'.$this->getArray($path).' = &$this->resolve($data);');
		}
		else
		{
			$ptr = $data;
		}

		return TRUE;
	}

	/**
	 * Set a value in the registry at the given key location (i.e. on a branch).
	 * @param string $path Path to branch (dot separated)
	 * @param string $key Path to key (relative to branch)
	 * @param mixed $data Data to store.
	 */
	public function kset($path, $key, $data) {
		$ptr = &$this->resolve($path);
		if(!$ptr) {
			$this->set($path, array());
			$ptr = &$this->resolve($path);
		}

		if(is_array($ptr)) {
			$ptr[$key] = $data;
		}
		else
		{
			return FALSE;
		}
	}

	/**
	 * Push data into an exisiting node/branch.
	 * @param string $path Path to branch (dot separated)
	 * @param mixed $data Data to push
	 */
	public function push($path, $data) {
		$ptr = &$this->resolve($path);
		if(!is_array($ptr)) {
			$tmp = $ptr;
			$ptr = array();
			$ptr[] = $tmp;
		}
		$ptr[] = $data;
	}

	/**
	 * Push data onto a branch, resolving to the given path.
	 * @param string $path The branch to start at
	 * @param string $key The key relative to the branch
	 * @param mixed $data Data to push
	 */
	public function kpush($path, $key, $data) {
		$ptr = &$this->resolve($path);
		if(!is_array($ptr[$key])) {
			$tmp = $ptr[$key];
			$ptr[$key] = array();
			$ptr[$key][] = $tmp;
		}
		$ptr[$key][] = $data;

	}

	/**
	 * Pop data of the end of a node.
	 * @param string $path Path to branch (dot separated)
	 */
	public function pop($path) {
		$ptr = &$this->resolve($path);
		
		if(!is_array($ptr)) {
			return $ptr;
		}

		return array_pop($ptr);
	}
	
	/**
	 * Get textual array representation of the path
	 * @param string $path Path to branch (dot separated)
	 * @return string
	 */
	public function getArray($path) {
		$hack = '';
		$tmp = explode(".", $path);
		foreach($tmp as $node) {
				$hack .= "['".$node."']";
		}
		return $hack;
	}

	/**
	 * Drop a path
	 * @param string $path Path to remove
	 */
	public function drop($path) {
		/* ew ew ew - why is there no way to unset a variable via a reference... */
		$hack = 'unset($this->registry'.$this->getArray($path);
		eval($hack.');');
	}

	/**
	 * Returns TRUE is true
	 * @param string $path Path
	 */
	public function true($path) {
		$ptr = &$this->resolve($path);
		return !!$ptr;
	}

	/**
	 * Pop data of the end of a node.
	 * @param string $path Path to branch (dot separated)
	 * @param string $key Key
	 */
	public function Kpop($path, $key) {
		$ptr = &$this->resolve($path);
		
		if(!isset($ptr[$key]) || !is_array($ptr[$key])) {
			return FALSE;
		}

		return array_pop($ptr[$key]);
	}

	/**
	 * Get the value or object found at the given path.
	 * The returned item may be a collection of nodes, or a leaf node.
	 * @param string $path Path (dot separated)
	 * @return mixed
	 */
	public function get($path) {
		$ret = $this->resolve($path);
		return $ret;
	}

	/**
	 * Get the value or object found at path, or set path to the alternativePath value.
	 * The returned item may be a collection of nodes, or a leaf node.
	 * @param string $path Path (dot separated)
	 * @param string $alternative_path Alternative Path (dot separated)
	 * @return mixed
	 */
	public function getOrSet($path, $alternative_path, $value='') {
		if($ret = $this->resolve($path)) {
			return $ret;
		}
		else if($alternative_path && ($ret = $this->resolve($alternative_path))) {
			$this->set($path, $ret);
			return $ret;
		}
		else
		{
			$this->set($path, $value);
			return $value;
		}
	}

	/**
	 * Get a value from a node starting from the given path.
	 * @param string $path Starting branch (dot separated)
	 * @param string $key Key (relative to branch)
	 * @return mixed
	 */
	public function kget($path, $key) {
		$tmp = &$this->resolve($path);
		if(isset($tmp[$key])) {
			return $tmp[$key];
		}
		else
			return FALSE;
	}

	/**
	 * Check if a value exists at the given path.
	 * @param string $path Path (dot separated)
	 * @param mixed $value Value to check
	 * @return boolean
	 */
	public function in($path, $value) {
		$haystack = &$this->resolve($path);

		if(is_array($haystack) && in_array($value, $haystack)) {
			return TRUE;
		}
		return FALSE;
	}

	/**
	 * Check if a value is found at the given path starting with the given branch.
	 * @param string $path Starting branch (dot separated)
	 * @param string $key Key (relative to branch)
	 * @param mixed $value Value
	 * @return boolean
	 */
	public function kin($path, $key, $value) {
		$ptr = &$this->resolve($path);

		if(!isset($ptr[$key]) || !is_array($ptr[$key]) || !in_array($value, $ptr[$key])) {
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * Update an existing value.
	 * @param string $path Path to value (dot separated)
	 * @param mixed $data New value
	 */
	public function update($path, $data) {
		if($ptr = &$this->resolve($path)) {
			$ptr = $data;
		}
		else
			return FALSE;

		return TRUE;
	}

	/**
	 * Update an existing value by key.
	 * @param string $path Starting branch (dot separated)
	 * @param string $key Key (relative to branch)
	 * @param mixed $value New value
	 */
	public function kupdate($path, $key, $value) {
		$ptr = &$this->resolve($path);
		if(isset($ptr[$key])) {
			$ptr[$key] = $value;
		}
		else
			return FALSE;

		return TRUE;
	}

	/**
	 * Check if a value exists, regardless of its value.
	 * @param string $paths Paths (dot separated) in a comma separated list
	 * @return boolean
	 */
	public function exists($paths) {
		$paths = explode(',', $paths);
		foreach($paths as $path) {
			$ptr = &$this->resolve($path);
			if(!isset($ptr)) return FALSE;
		}
		return TRUE;
	}

	/**
	 * Delete a value completely from the given node.
	 * @param string $path Path to node (dot separated)
	 */
	public function delete($path) {
		if($ptr = &$this->resolve($path)) {
			$ptr = array();
		}
		else
			return FALSE;

		return TRUE;
	}

	/**
	 * Delete a value completely from the given node by key.
	 * @param string $path Path to node (dot separated)
	 * @param string $key Key (relative to branch)
	 */
	public function kdelete($path, $key) {
		$ptr = &$this->resolve($path);
		if(isset($ptr[$key])) {
			$ptr[$key] = array();
		}
		else
			return FALSE;

		return TRUE;
	}

	/**
	 * Increment a nodes value
	 * @param string $path Path to node (dot separated)
	 */
	public function inc($path) {
		$ptr = &$this->resolve($path);
		if(is_numeric($ptr)) {
			$ptr++;
		}
	}

	/**
	 * Return a new Nina_Registry that has a copy of the path specified.
	 * @param string [$path=''] Path to node (dot separated).
	 * @return Nina_Registry
	 */
	public function getCopy($path='') {
		$ret = new self();
		$ptr = $this->resolve($path);

		if(!is_array($ptr)) {
			throw new Nina_Exception_Registry("Unable to getCopy on value (leaf node). Pass getCopy a path that has depth.");
		}

		$ret->restore($ptr);
		return $ret;
	}

	/**
	 * Return a new Nina_Registry that references the data in the path specified.
	 * @param string [$path=''] Path to node (dot separated).
	 * @return Nina_Registry
	 */
	public function getReference($path='') {
		$ptr = &$this->resolve($path);
		$ret = new self();

		if(!is_array($ptr)) {
			throw new Nina_Exception_Registry("Unable to getReference on value (leaf node). Pass getReference a path that has depth.");
		}

		$ret->restoreByReference($ptr);
		return $ret;
	}

	/**
	 * Dump the contents of the given node in a raw format.
	 * @param string [$path=''] Path to node (dot separated).
	 * @return string
	 */
	public function dump($path='') {
			$ptr = &$this->resolve($path);
			return $ptr;
	}

	/**
	 * Purge registry
	 */
	public function purge() {
		$this->registry = array();
	}

	/**
	 * Restore the registry from an existing copy of a dump.
	 * @param array $data Dump data
	 */
	public function restore($data) {
		$this->registry = $data;
	}

	/**
	 * Restore the registry from an existing copy of a dump by reference.
	 * @param array $data Dump data
	 */
	public function restoreByReference(&$data) {
		$this->registry = &$data;
	}
}

/*
require_once('Exception.php');
require_once('Exception/Registry.php');

$reg = new Nina_Registry();
$reg->set('test.db.instance', 'test');
$reg->set('test.db.username', 'root');

$reg->set('*db.pointer', 'test.db');

print_r($reg->dump('db.pointer'))."\n";

*/
